package com.google.srclib.io.cvs;

import java.io.IOException;
import java.io.InputStream;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;

import org.h2.engine.Constants;
import org.h2.tools.SimpleResultSet;
import org.h2.util.New;

/**
 * 
 * @author YLiang
 * 
 */
public class CSVImporter {
	private char fieldSeparatorRead = ',';
	private char commentLineStart = '#';
	private static char fieldDelimiter = '\"';
	private static char escapeCharacter = '\"';
	private static String nullString = "";
	private String[] columnNames;
	private int inputBufferPos;
	private int inputBufferStart = -1;
	private int inputBufferEnd;
	private byte[] inputBuffer;
	private boolean endOfLine, endOfFile;
	private static CSVImporter singleton = null;

	public static CSVImporter getInstance() {
		if (singleton == null) {
			singleton = new CSVImporter();
		}
		return singleton;
	}

	/**
	 * @param in
	 * @return ResultSet
	 * 
	 * @throws SQLException
	 * @throws IOException
	 */
	public ResultSet csvReader(InputStream in) throws SQLException, IOException {
		reset();
		ResultSet rs = readResultSet(in, null);
		return rs;
	}

	private ResultSet readResultSet(InputStream zin, String[] colNames)
			throws SQLException, IOException {
		this.columnNames = colNames;
		initRead(zin);
		SimpleResultSet result = new SimpleResultSet();
		makeColumnNamesUnique();
		for (String columnName : columnNames) {
			result.addColumn(columnName, Types.VARCHAR, Integer.MAX_VALUE, 0);
		}
		Object[] row;
		while ((row = readRow(zin)) != null)
			result.addRow(row);
		return result;
	}

	private void makeColumnNamesUnique() {
		for (int i = 0; i < columnNames.length; i++) {
			String x = columnNames[i];
			if (x == null || x.length() == 0) {
				x = "C" + (i + 1);
			}
			for (int j = 0; j < i; j++) {
				String y = columnNames[j];
				if (x.equals(y)) {
					x += "1";
					j = -1;
				}
			}
			columnNames[i] = x;
		}
	}

	private void initRead(InputStream zin) throws IOException {
		inputBuffer = new byte[Constants.IO_BUFFER_SIZE * 2];
		if (columnNames == null) {
			readHeader(zin);
		}

	}

	private void readHeader(InputStream zin) throws IOException {
		ArrayList<String> list = New.arrayList();
		while (true) {
			String v = readValue(zin);
			if (v == null) {
				if (endOfLine) {
					if (endOfFile || list.size() > 0) {
						break;
					}
				} else {
					v = "COLUMN" + list.size();
					list.add(v);
				}
			} else {
				if (v.length() == 0) {
					v = "COLUMN" + list.size();
				} else if (isSimpleColumnName(v)) {
					v = v.toUpperCase();
				}
				list.add(v);
				if (endOfLine) {
					break;
				}
			}
		}
		columnNames = new String[list.size()];
		list.toArray(columnNames);
	}

	private String readValue(InputStream zin) throws IOException {
		endOfLine = false;
		inputBufferStart = inputBufferPos;
		while (true) {
			int ch = readChar(zin);
			if (ch == fieldDelimiter) {
				// delimited value
				boolean containsEscape = false;
				inputBufferStart = inputBufferPos;
				int sep;
				while (true) {
					ch = readChar(zin);
					if (ch == fieldDelimiter) {
						ch = readChar(zin);
						if (ch != fieldDelimiter) {
							sep = 2;
							break;
						}
						containsEscape = true;
					} else if (ch == escapeCharacter) {
						ch = readChar(zin);
						if (ch < 0) {
							sep = 1;
							break;
						}
						containsEscape = true;
					} else if (ch < 0) {
						sep = 1;
						break;
					}
				}
				String s = new String(inputBuffer, inputBufferStart,
						inputBufferPos - inputBufferStart - sep);
				if (containsEscape) {
					s = unEscape(s);
				}
				inputBufferStart = -1;
				while (true) {
					if (ch == fieldSeparatorRead) {
						break;
					} else if (ch == '\n' || ch < 0 || ch == '\r') {
						endOfLine = true;
						break;
					} else if (ch == ' ' || ch == '\t') {
						// ignore
					} else {
						pushBack();
						break;
					}
					ch = readChar(zin);
				}
				return s;
			} else if (ch == '\n' || ch < 0 || ch == '\r') {
				endOfLine = true;
				return null;
			} else if (ch == fieldSeparatorRead) {
				// null
				return null;
			} else if (ch <= ' ') {
				// ignore spaces
				continue;
			} else if (ch == commentLineStart) {
				// comment until end of line
				inputBufferStart = -1;
				while (true) {
					ch = readChar(zin);
					if (ch == '\n' || ch < 0 || ch == '\r') {
						break;
					}
				}
				endOfLine = true;
				return null;
			} else {
				// un-delimited value
				while (true) {
					ch = readChar(zin);
					if (ch == fieldSeparatorRead) {
						break;
					} else if (ch == '\n' || ch < 0 || ch == '\r') {
						endOfLine = true;
						break;
					}
				}
				String s = new String(inputBuffer, inputBufferStart,
						inputBufferPos - inputBufferStart - 1);
				inputBufferStart = -1;
				// check un-delimited value for nullString
				return readNull(s.trim());
			}
		}
	}

	private void pushBack() {
		inputBufferPos--;
	}

	private int readChar(InputStream zin) throws IOException {
		if (inputBufferPos >= inputBufferEnd) {
			return readBuffer(zin);
		}
		return inputBuffer[inputBufferPos++];
	}

	private void reset() {
		endOfFile = false;
	}

	private int readBuffer(InputStream zin) throws IOException {
		if (endOfFile) {
			return -1;
		}
		int keep;
		if (inputBufferStart >= 0) {
			keep = inputBufferPos - inputBufferStart;
			if (keep > 0) {
				byte[] src = inputBuffer;
				if (keep + Constants.IO_BUFFER_SIZE > src.length) {
					inputBuffer = new byte[src.length * 2];
				}
				System.arraycopy(src, inputBufferStart, inputBuffer, 0, keep);
			}
			inputBufferStart = 0;
		} else {
			keep = 0;
		}
		inputBufferPos = keep;
		int len = zin.read(inputBuffer, keep, Constants.IO_BUFFER_SIZE);
		if (len == -1) {
			// ensure bufferPos > bufferEnd
			// even after pushBack
			inputBufferEnd = -1024;
			endOfFile = true;
			// ensure the right number of characters are read
			// in case the input buffer is still used
			inputBufferPos++;
			return -1;
		}
		inputBufferEnd = keep + len;
		return inputBuffer[inputBufferPos++];
	}

	private String readNull(String s) {
		return s.equals(nullString) ? null : s;
	}

	private String unEscape(String s) {
		StringBuilder buff = new StringBuilder(s.length());
		int start = 0;
		char[] chars = null;
		while (true) {
			int idx = s.indexOf(escapeCharacter, start);
			if (idx < 0) {
				idx = s.indexOf(fieldDelimiter, start);
				if (idx < 0) {
					break;
				}
			}
			if (chars == null) {
				chars = s.toCharArray();
			}
			buff.append(chars, start, idx - start);
			if (idx == s.length() - 1) {
				start = s.length();
				break;
			}
			buff.append(chars[idx + 1]);
			start = idx + 2;
		}
		buff.append(s.substring(start));
		return buff.toString();
	}

	private boolean isSimpleColumnName(String columnName) {
		for (int i = 0; i < columnName.length(); i++) {
			char ch = columnName.charAt(i);
			if (i == 0) {
				if (ch != '_' && !Character.isLetter(ch)) {
					return false;
				}
			} else {
				if (ch != '_' && !Character.isLetterOrDigit(ch)) {
					return false;
				}
			}
		}
		if (columnName.length() == 0) {
			return false;
		}
		return true;
	}

	private Object[] readRow(InputStream zin) throws SQLException {
		String[] row = new String[columnNames.length];
		try {
			int i = 0;
			while (true) {
				String v = readValue(zin);
				if (v == null) {
					if (endOfLine) {
						if (i == 0) {
							if (endOfFile) {
								return null;
							}
							// empty line
							continue;
						}
						break;
					}
				}
				if (i < row.length) {
					row[i++] = v;
				}
				if (endOfLine) {
					break;
				}
			}
		} catch (IOException e) {
			throw convertException("IOException reading!", e);
		}
		return row;
	}

	private SQLException convertException(String message, Exception e) {
		SQLException s = new SQLException(message, "CSV");
		// ## Java 1.4 begin ##
		s.initCause(e);
		// ## Java 1.4 end ##
		return s;
	}

}
